图文混排的四种方案

图文混排有多种方式可以实现,下面我会用四种不同的方式来实现以下界面的效果,并且说明他们的优缺点。

screenshot

NSAttributedString

NSAttributedString提供了自由并且多样式的富文本设置,图文混排的实现是通过插入NSTestAttachment来实现。

talk is cheap ,here is the code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//调用
NSString *originStr = @" Hi ALL,这是我的博客:lemon2well.top,欢迎来到我的博客,环欢迎评论留言一起交流。";
NSString *targetStr = @"lemon2well.top";
NSString *imageName = @"trumpet";
self.label.attributedText = [self attribuStringWithString:originStr targetString:targetStr image:imageName];

//方法
- (NSAttributedString*)attribuStringWithString:(NSString*)string targetString:(NSString *)target image:(NSString*)imageName{
NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc]initWithString:string attributes:@{NSForegroundColorAttributeName:[UIColor blackColor],NSFontAttributeName:[UIFont systemFontOfSize:20]}];

NSRange linkRange = [string rangeOfString:target];
if (linkRange.length != 0) {
//添加可点击链接
[attributedStr addAttribute:NSLinkAttributeName value:[NSURL URLWithString:@"lemon2well.top"] range:linkRange];
[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:linkRange];
//添加下划线
[attributedStr addAttribute:NSUnderlineStyleAttributeName value:@1 range:linkRange];
}

//添加图片
NSTextAttachment *attachment = [[NSTextAttachment alloc]init];
attachment.image = [UIImage imageNamed:imageName];
attachment.bounds = CGRectMake(0, 0, 20 ,20);
NSAttributedString *imageStr = [NSAttributedString attributedStringWithAttachment:attachment];
//插入图片
[attributedStr insertAttributedString:imageStr atIndex:0];

// 段落样式
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc]init];
// 行间距
[style setLineSpacing:3];
// 段落间距
[style setParagraphSpacing:10];
// 首行缩进
[style setFirstLineHeadIndent:25];
[attributedStr addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, string.length-1)];

return attributedStr;
}

以下是NSAttributedString的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// NSFontAttributeName                设置字体属性,默认值:字体:Helvetica(Neue) 字号:12
// NSForegroundColorAttributeNam 设置字体颜色,取值为 UIColor对象,默认值为黑色
// NSBackgroundColorAttributeName 设置字体所在区域背景颜色,取值为 UIColor对象,默认值为nil, 透明色
// NSLigatureAttributeName 设置连体属性,取值为NSNumber 对象(整数),0 表示没有连体字符,1 表示使用默认的连体字符
// NSKernAttributeName 设定字符间距,取值为 NSNumber 对象(整数),正值间距加宽,负值间距变窄
// NSStrikethroughStyleAttributeName 设置删除线,取值为 NSNumber 对象(整数)
// NSStrikethroughColorAttributeName 设置删除线颜色,取值为 UIColor 对象,默认值为黑色
// NSUnderlineStyleAttributeName 设置下划线,取值为 NSNumber 对象(整数),枚举常量 NSUnderlineStyle中的值,与删除线类似
// NSUnderlineColorAttributeName 设置下划线颜色,取值为 UIColor 对象,默认值为黑色
// NSStrokeWidthAttributeName 设置笔画宽度,取值为 NSNumber 对象(整数),负值填充效果,正值中空效果
// NSStrokeColorAttributeName 填充部分颜色,不是字体颜色,取值为 UIColor 对象
// NSShadowAttributeName 设置阴影属性,取值为 NSShadow 对象
// NSTextEffectAttributeName 设置文本特殊效果,取值为 NSString 对象,目前只有图版印刷效果可用:
// NSBaselineOffsetAttributeName 设置基线偏移值,取值为 NSNumber (float),正值上偏,负值下偏
// NSObliquenessAttributeName 设置字形倾斜度,取值为 NSNumber (float),正值右倾,负值左倾
// NSExpansionAttributeName 设置文本横向拉伸属性,取值为 NSNumber (float),正值横向拉伸文本,负值横向压缩文本
// NSWritingDirectionAttributeName 设置文字书写方向,从左向右书写或者从右向左书写
// NSVerticalGlyphFormAttributeName 设置文字排版方向,取值为 NSNumber 对象(整数),0 表示横排文本,1 表示竖排文本
// NSLinkAttributeName 设置链接属性,点击后调用浏览器打开指定URL地址
// NSAttachmentAttributeName 设置文本附件,取值为NSTextAttachment对象,常用于文字图片混排
// NSParagraphStyleAttributeName 设置文本段落排版格式,取值为 NSParagraphStyle 对象

NSParagraphStyle的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];  
paragraphStyle.lineSpacing = 10;// 字体的行间距
paragraphStyle.firstLineHeadIndent = 20.0f;//首行缩进
paragraphStyle.alignment = NSTextAlignmentJustified;//(两端对齐的)文本对齐方式:(左,中,右,两端对齐,自然)
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;//结尾部分的内容以……方式省略 ( "...wxyz" ,"abcd..." ,"ab...yz")
paragraphStyle.headIndent = 20;//整体缩进(首行除外)
paragraphStyle.tailIndent = 20;//
paragraphStyle.minimumLineHeight = 10;//最低行高
paragraphStyle.maximumLineHeight = 20;//最大行高
paragraphStyle.paragraphSpacing = 15;//段与段之间的间距
paragraphStyle.paragraphSpacingBefore = 22.0f;//段首行空白空间/* Distance between the bottom of the previous paragraph (or the end of its paragraphSpacing, if any) and the top of this paragraph. */
paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight;//从左到右的书写方向(一共➡️三种)
paragraphStyle.lineHeightMultiple = 15;/* Natural line height is multiplied by this factor (if positive) before being constrained by minimum and maximum line height. */
paragraphStyle.hyphenationFactor = 1;//连字属性 在iOS,唯一支持的值分别为0和1

TextView + UIImageview

以上的效果同样可以使用UITextView + UIImageView来实现,该方法主要利用了textview的textContainer.exclusionPaths属性

// Default value : empty array An array of UIBezierPath representing the exclusion paths inside the receiver’s bounding rect.
@property (copy, NS_NONATOMIC_IOSONLY) NSArray *exclusionPaths NS_AVAILABLE(10_11, 7_0);

按照文档的说明我们可以提供一个元素为UIBezierPath的数组,这样文字的描绘就会避开这些路径。从而实现图文混排的效果。不过这种方式最适合用于文字环绕的效果,所有的文字把图片包围住。

here is the code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 调用
UITextView *textView = [[UITextView alloc]init];
textView.frame = CGRectMake(20, 200, 335, 150);
textView.attributedText = [self textViewAttributestring];
[textView sizeToFit];
[self.view addSubview:textView];
self.textView = textView;

UIImageView *imageView = [[UIImageView alloc]init];
imageView.frame = CGRectMake(45, 210, 20, 20);
imageView.image = [UIImage imageNamed:@"trumpet"];
[self.view addSubview:imageView];
self.imageView = imageView;
textView.textContainer.exclusionPaths = @[[self translatedBezierPath]];

//方法:
- (UIBezierPath *)translatedBezierPath
{
//计算出imageView相对于textView的相对坐标
CGRect imageRect = [self.textView
convertRect:self.imageView.frame fromView:self.view];
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(imageRect.origin.x, imageRect.origin.y-10, imageRect.size.width, imageRect.size.height)];
return bezierPath;
}


- (NSAttributedString*)textViewAttributestring{
NSString *originStr = @" Hi ALL,这是我的博客:lemon2well.top,欢迎来到我的博客,环欢迎评论留言一起交流。";
NSString *targetStr = @"lemon2well.top";

NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc]initWithString:originStr attributes:@{NSForegroundColorAttributeName:[UIColor blackColor],NSFontAttributeName:[UIFont systemFontOfSize:20]}];

NSRange linkRange = [originStr rangeOfString:targetStr];
if (linkRange.length != 0) {
//添加可点击链接
[attributedStr addAttribute:NSLinkAttributeName value:[NSURL URLWithString:targetStr] range:linkRange];
[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:linkRange];
//添加下划线
[attributedStr addAttribute:NSUnderlineStyleAttributeName value:@1 range:linkRange];
}

// 段落样式
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc]init];
// 行间距
[style setLineSpacing:3];
// 段落间距
[style setParagraphSpacing:10];
// 首行缩进
[style setFirstLineHeadIndent:25];
[attributedStr addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, originStr.length-1)];

return attributedStr;
}

CoreText

coreText给我的感觉就是太难用了,但是可创造性又很高,因为CoreText是属于比较底层的框架,所以基本上都是使用的C的方法,CoreText可以实现很复杂的图文混排而且渲染速度更快。

CoreText来实现图文混排实际上分为以下的几步:

获得上下文,翻转坐标系 -> 创建NSAttributeString -> 创建空白占位图片,创建代理 -> 实现代理 -> 创建CTFrameRef,CTFrameDraw绘制 -> 计算图片坐标,CGContextDrawImage绘制 -> 释放资源

CFAttributedStringRef :属性字符串,用于存储需要绘制的文字字符和字符属性
CTFramesetterRef:通过CFAttributedStringRef进行初始化,作为CTFrame对象的生产工厂,负责根据path创建对应的CTFrame
CTFrame:用于绘制文字的类,可以通过CTFrameDraw函数,直接将文字绘制到context上
CTLine:在CTFrame内部是由多个CTLine来组成的,每个CTLine代表一行
CTRun:每个CTLine又是由多个CTRun组成的,每个CTRun代表一组显示风格一致的文本
实际上CoreText是不直接支持绘制图片的,但是我们可以先在需要显示图片的地方用一个特殊的空白占位符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度,这样绘制文字的时候就会先把图片的位置留出来,再在drawRect方法里面用CGContextDrawImage绘制图片。

Here is the code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
-(void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@" Hi ALL,这是我的博客:lemon2well.top,欢迎来到我的博客,环欢迎评论留言一起交流。" attributes:@{NSForegroundColorAttributeName:[UIColor blackColor],NSFontAttributeName:[UIFont systemFontOfSize:20]}];
NSString *targetStr = @"lemon2well.top";

NSRange linkRange = [attributeStr.string rangeOfString:targetStr];
if (linkRange.length != 0) {
//添加可点击链接
[attributeStr addAttribute:NSLinkAttributeName value:[NSURL URLWithString:targetStr] range:linkRange];
[attributeStr addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:linkRange];
//添加下划线
[attributeStr addAttribute:NSUnderlineStyleAttributeName value:@1 range:linkRange];
}

// 段落样式
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc]init];
// 行间距
[style setLineSpacing:3];
// 段落间距
[style setParagraphSpacing:10];
// 首行缩进
[style setFirstLineHeadIndent:25];
[attributeStr addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, attributeStr.length-1)];


CTRunDelegateCallbacks callBacks;
memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));
callBacks.version = kCTRunDelegateVersion1;
callBacks.getAscent = ascentCallBacks;
callBacks.getDescent = descentCallBacks;
callBacks.getWidth = widthCallBacks;
NSDictionary * dicPic = @{@"height":@16,@"width":@16};
CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);
unichar placeHolder = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
[attributeStr insertAttributedString:placeHolderAttrStr atIndex:0];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
NSInteger length = attributeStr.length;
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);
CTFrameDraw(frame, context);

UIImage * image = [UIImage imageNamed:@"trumpet"];
CGRect imgFrm = [self calculateImageRectWithFrame:frame];
CGContextDrawImage(context,imgFrm, image.CGImage);
CFRelease(frame);
CFRelease(path);
CFRelease(frameSetter);
}
static CGFloat ascentCallBacks(void * ref)
{
return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue];
}
static CGFloat descentCallBacks(void * ref)
{
return 0;
}
static CGFloat widthCallBacks(void * ref)
{
return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue];
}


-(CGRect)calculateImageRectWithFrame:(CTFrameRef)frame
{
NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);
NSInteger count = [arrLines count];
CGPoint points[count];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
for (int i = 0; i < count; i ++) {
CTLineRef line = (__bridge CTLineRef)arrLines[i];
NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < arrGlyphRun.count; j ++) {
CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];
NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run); CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
NSDictionary * dic = CTRunDelegateGetRefCon(delegate);
if (![dic isKindOfClass:[NSDictionary class]]) {
continue;
}
CGPoint point = points[i];
CGFloat ascent;
CGFloat descent;
CGRect boundsRun;
boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
boundsRun.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
boundsRun.origin.x = point.x + xOffset;
boundsRun.origin.y = point.y - descent;
CGPathRef path = CTFrameGetPath(frame);
CGRect colRect = CGPathGetBoundingBox(path);
CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);
return imageBounds;
}
}
return CGRectZero;
}

YYText

YYText
YYText是大神郭曜源开发的一个强大的展示和编辑富文本的第三方工具,里面提供了丰富的与富文本开发相关的方法,具体的可以到github里面看。YYText是基于CoreText向上封装了一层,所以对开发者更加友好,如果在项目中运用到大量的富文本的地方建议可以用YYText。

here is the code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
YYLabel *label = [[YYLabel alloc]init];
label.numberOfLines = 0 ;
label.frame = CGRectMake(0, 200, 375, 200);
[self.view addSubview:label];

NSString *headStr = @"Hi ALL,这是我的博客:";
NSString *linkStr = @"lemon2well.top";
NSString *lastStr = @",欢迎来到我的博客,环欢迎评论留言一起交流。";
UIImage *image = [UIImage imageNamed:@"trumpet"];

NSMutableAttributedString *attrText = [NSMutableAttributedString new];

NSAttributedString *headAttr = [[NSAttributedString alloc] initWithString:headStr attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:20]}];

NSAttributedString *lastAttr = [[NSAttributedString alloc] initWithString:lastStr attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:20]}];

NSMutableAttributedString *linkAttr = [[NSMutableAttributedString alloc] initWithString:linkStr];
linkAttr.yy_font = [UIFont systemFontOfSize:20];
linkAttr.yy_underlineStyle = NSUnderlineStyleSingle;
[linkAttr yy_setTextHighlightRange:NSMakeRange(0, linkStr.length) color:[UIColor blueColor] backgroundColor:nil tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
NSLog(@"link = %@",text);
}];

NSMutableAttributedString *imageAttr = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:CGSizeMake(32, 32) alignToFont:[UIFont systemFontOfSize:20] alignment:YYTextVerticalAlignmentCenter];

[attrText appendAttributedString:imageAttr];
[attrText appendAttributedString:headAttr];
[attrText appendAttributedString:linkAttr];
[attrText appendAttributedString:lastAttr];

label.attributedText = attrText;

总结:

总的来说,以上说的四种都各有优劣,但是他们都用了NSAttributedString来实现富文本。

如果在项目中用到富文本的地方不多,出于APP体积考虑没必要引入一个第三方, 可以考虑使用第一和第二种方案,如果是图文环绕这种,可以使用UITextView+UIImageview的方案,如果是小的表情图文混排可以使用NSAttributedString+label即可。

如果在项目中多处用到富文本的展示和编辑,建议使用YYText,因为它对于开发者更加友好,并且也是基于CoreText来渲染,不过有一点就是目前YYText已经有一年没维护了,之前他生病了,现在正在家里修养,希望他快点好起来,祝好。

突然有感

本来这篇文章到这里就应该完了,不过刚刚去看了一下YYKit的作者的博客,突然有点伤感,没想到他生病这么严重,看他的文字感觉他是一个对待生活也很细腻的人,这也说得通为什么他可以用业余的时间写出了YYKIT这么强大的工具,因为他对待技术也是很细腻。

真的很佩服这样的人,你可以感受到他的真诚,我虽然没和他聊过天,没见过他真人,不过他一定会是一个让别人感受到交流沟通起来让你没有防御的一个人。现在这个社会因为各种各样变态的人以及事,总会让人无论何时何地总会身上架着一层防御装,更可怕的是还有那种表面善内里恶的人,多可怕,如果是危害别人的人我一点都不心疼。

最后希望大家一定要注意自己的身体,祝大家都好。

-------评论系统采用disqus,如果看不到需要翻墙-------------